From db47037bdc23ed0e8c2613207aca03ceb7994d4e Mon Sep 17 00:00:00 2001 From: Andreas Niemann Date: Tue, 30 Jun 2026 20:34:55 +0200 Subject: [PATCH] feat(offsite-backup): olsitec-foundation bucket + scoped creds on home MinIO CONTRACT_004 offsite target (ADR-004 'second self-hosted location'). @pulumi/minio program (modeled on olsicloud4 modules/minio): bucket 'olsitec-foundation' + scoped IAM user/policy + service account on minio.wob.olsitec.de:19000. Verified: scoped SA can put/list/delete in its bucket, DENIED cross-bucket. Admin creds + scoped creds via ENV/state only (gitignored), never committed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + bun.lock | 16 ++++ .../2026-06-30_foundation-bootstrap.log | 3 + offsite-backup/Pulumi.yaml | 6 ++ offsite-backup/index.ts | 86 +++++++++++++++++++ offsite-backup/package.json | 6 ++ offsite-backup/tsconfig.json | 2 + package.json | 3 +- 8 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 offsite-backup/Pulumi.yaml create mode 100644 offsite-backup/index.ts create mode 100644 offsite-backup/package.json create mode 100644 offsite-backup/tsconfig.json diff --git a/.gitignore b/.gitignore index 2245353..2799a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ bootstrap/state/ # os .DS_Store provision/state/ +offsite-backup/state/ +offsite-backup/Pulumi.prod.yaml +provision/Pulumi.foundation-test.yaml diff --git a/bun.lock b/bun.lock index cbd5ba1..966246b 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,18 @@ "typescript": "^5.0.0", }, }, + "offsite-backup": { + "name": "@olsitec/foundation-offsite-backup", + "version": "0.0.0", + "dependencies": { + "@pulumi/minio": "^0.16.0", + "@pulumi/pulumi": "^3.138.0", + }, + "devDependencies": { + "@types/node": "^18", + "typescript": "^5.0.0", + }, + }, "packages/pulumi-docker": { "name": "@olsitec/pulumi-docker", "version": "0.0.0", @@ -160,6 +172,8 @@ "@olsitec/foundation-bootstrap": ["@olsitec/foundation-bootstrap@workspace:bootstrap"], + "@olsitec/foundation-offsite-backup": ["@olsitec/foundation-offsite-backup@workspace:offsite-backup"], + "@olsitec/foundation-provision": ["@olsitec/foundation-provision@workspace:provision"], "@olsitec/pulumi-docker": ["@olsitec/pulumi-docker@workspace:packages/pulumi-docker"], @@ -230,6 +244,8 @@ "@pulumi/hcloud": ["@pulumi/hcloud@1.39.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-rrjOZ1bPliOpsuoGBrd6b9GOeM+CoNSLTJrd061JzwAREdztVP6vy8UEROQj7zIUypEI0+eCqXAA1bxYIQSwkQ=="], + "@pulumi/minio": ["@pulumi/minio@0.16.9", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-druJ9i1edmXbzTTyHaH2W5xK2BRB4k4O02jTV6FBk1cRp8na9y5dDIrzWjDTRTEqXSRjSNruEWzltyj6Bh2aVg=="], + "@pulumi/pulumi": ["@pulumi/pulumi@3.248.0", "", { "dependencies": { "@grpc/grpc-js": "^1.10.1", "@logdna/tail-file": "^2.0.6", "@npmcli/arborist": "^9.0.0", "@opentelemetry/api": "^1.9", "@opentelemetry/exporter-trace-otlp-grpc": "^0.57", "@opentelemetry/exporter-zipkin": "^1.30", "@opentelemetry/instrumentation": "^0.57", "@opentelemetry/instrumentation-grpc": "^0.57", "@opentelemetry/resources": "^1.30", "@opentelemetry/sdk-trace-base": "^1.30", "@opentelemetry/sdk-trace-node": "^1.30", "@types/google-protobuf": "^3.15.5", "@types/semver": "^7.5.6", "@types/tmp": "^0.2.6", "execa": "^5.1.0", "fdir": "^6.5.0", "google-protobuf": "^3.21.4", "ini": "^2.0.0", "js-yaml": "^4.0.0", "minimist": "^1.2.6", "normalize-package-data": "^6.0.0", "picomatch": "^4.0.0", "require-from-string": "^2.0.1", "semver": "^7.5.2", "source-map-support": "^0.5.6", "tmp": "^0.2.4", "upath": "^1.1.0" }, "peerDependencies": { "ts-node": ">= 7.0.1 < 12", "typescript": ">= 3.8.3 < 7" }, "optionalPeers": ["ts-node", "typescript"] }, "sha512-EqgeHjVIqMS8voAM7F8SOzFAMHnVXUDdKTNF1o3Lg85YwVI0j4/eIlWG0iIVAWJl3DX0KOOM6++X0wLKHWWwmQ=="], "@pulumi/random": ["@pulumi/random@4.21.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-k4JHqQFOKXWkbSLZtX37xBlRsurfgZbLd1Zib0cLSDP4Dt3uTllR3CBWfMLSnXJGAoTr9MlwyHCer/5tmSaF0Q=="], diff --git a/documentation/command-log/2026-06-30_foundation-bootstrap.log b/documentation/command-log/2026-06-30_foundation-bootstrap.log index e6c5faf..3664b7c 100644 --- a/documentation/command-log/2026-06-30_foundation-bootstrap.log +++ b/documentation/command-log/2026-06-30_foundation-bootstrap.log @@ -29,3 +29,6 @@ EXIT: 0 — test VM foundation-test @ 91.98.117.152 (cx23, nbg1-dc3), SSH:222, D NOTE: docker-over-SSH provider path needs SSH_PRIVATE_KEY_PATH=~/.ssh/foundation-test_ed25519 + FOUNDATION_DOCKER_HOST=ssh://root@91.98.117.152:222. DESTROY: cd provision && pulumi destroy (stack foundation-test). --- --- 2026-06-30T18:13:36Z --- CMD: pulumi up (cx33/hel1-dc2 replace) EXIT: RUNNING +--- 2026-06-30T18:32:52Z --- HOST: mac->minio.wob.olsitec.de:19000 CMD: pulumi up (olsitec-foundation bucket + scoped SA) EXIT: RUNNING NOTE: offsite backup target setup +--- 2026-06-30T18:32:54Z UPDATE --- EXIT: 0 — bucket+scoped SA created on home MinIO +--- 2026-06-30T18:34:55Z UPDATE --- EXIT: 0 — olsitec-foundation bucket + scoped SA verified (put/list/delete OK, cross-bucket DENIED). diff --git a/offsite-backup/Pulumi.yaml b/offsite-backup/Pulumi.yaml new file mode 100644 index 0000000..1907b83 --- /dev/null +++ b/offsite-backup/Pulumi.yaml @@ -0,0 +1,6 @@ +name: foundation-offsite-backup +description: Offsite backup target — olsitec-foundation bucket + scoped creds on the home Synology MinIO (CONTRACT_004 offsite). +runtime: + name: nodejs + options: + packagemanager: bun diff --git a/offsite-backup/index.ts b/offsite-backup/index.ts new file mode 100644 index 0000000..7375480 --- /dev/null +++ b/offsite-backup/index.ts @@ -0,0 +1,86 @@ +// offsite-backup/index.ts +// +// Offsite backup target (CONTRACT_004 §4.1; ADR-004 "second self-hosted location"): +// the `olsitec-foundation` bucket + a SCOPED service account on the home Synology +// DS923+ MinIO. The Helsinki foundation VM pushes backup bundles here with the +// scoped creds (never the MinIO root). Modeled on olsicloud4 modules/minio. +// +// Admin creds via ENV (export MINIO_BACKUP_USER/MINIO_BACKUP_PASSWORD from pass), +// never committed. Endpoint is the PUBLIC one (reachable from Hetzner). +import * as pulumi from "@pulumi/pulumi"; +import * as minio from "@pulumi/minio"; + +const adminUser = process.env.MINIO_BACKUP_USER; +const adminPass = process.env.MINIO_BACKUP_PASSWORD; +if (!adminUser || !adminPass) { + throw new Error( + 'MINIO_BACKUP_USER/MINIO_BACKUP_PASSWORD env required: export from pass olsicloud4/MINIO_BACKUP_*', + ); +} + +const endpoint = "minio.wob.olsitec.de:19000"; // public S3 API (-> 62.176.248.112) +const bucketName = "olsitec-foundation"; + +const provider = new minio.Provider("home-minio", { + minioServer: endpoint, + minioUser: adminUser, + minioPassword: adminPass, + minioSsl: true, +}); +const popts = { provider }; + +const bucket = new minio.S3Bucket( + "foundation-backup-bucket", + { + bucket: bucketName, + acl: "private", + forceDestroy: false, // holds backups — never silently wipe on destroy + }, + popts, +); + +// Scoped policy: full object ops on this bucket only. +const policy = new minio.IamPolicy( + "foundation-backup-policy", + { + name: "olsitec-foundation-backup", + policy: bucket.bucket.apply((b) => + JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: ["s3:ListBucket", "s3:GetBucketLocation", "s3:ListBucketMultipartUploads"], + Resource: [`arn:aws:s3:::${b}`], + }, + { + Effect: "Allow", + Action: ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload"], + Resource: [`arn:aws:s3:::${b}/*`], + }, + ], + }), + ), + }, + popts, +); + +const user = new minio.IamUser("foundation-backup-user", { name: "olsitec-foundation-backup" }, popts); + +new minio.IamUserPolicyAttachment( + "foundation-backup-user-policy", + { userName: user.name, policyName: policy.name }, + { ...popts, dependsOn: [bucket, policy, user] }, +); + +// Service account = the scoped access/secret key pair the VM actually uses. +const sa = new minio.IamServiceAccount( + "foundation-backup-sa", + { targetUser: user.name }, + { ...popts, dependsOn: [user] }, +); + +export const offsiteEndpoint = `https://${endpoint}`; +export const offsiteBucket = bucket.bucket; +export const offsiteAccessKey = pulumi.secret(sa.accessKey); +export const offsiteSecretKey = pulumi.secret(sa.secretKey); diff --git a/offsite-backup/package.json b/offsite-backup/package.json new file mode 100644 index 0000000..2da653b --- /dev/null +++ b/offsite-backup/package.json @@ -0,0 +1,6 @@ +{ + "name": "@olsitec/foundation-offsite-backup", + "private": true, "version": "0.0.0", "main": "index.ts", + "dependencies": { "@pulumi/minio": "^0.16.0", "@pulumi/pulumi": "^3.138.0" }, + "devDependencies": { "@types/node": "^18", "typescript": "^5.0.0" } +} diff --git a/offsite-backup/tsconfig.json b/offsite-backup/tsconfig.json new file mode 100644 index 0000000..0e96839 --- /dev/null +++ b/offsite-backup/tsconfig.json @@ -0,0 +1,2 @@ +{ "compilerOptions": { "strict": true, "outDir": "bin", "target": "es2020", "module": "commonjs", + "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true }, "files": ["index.ts"] } diff --git a/package.json b/package.json index 2a4be8f..cb5bf80 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "workspaces": [ "packages/*", "bootstrap", - "provision" + "provision", + "offsite-backup" ], "devDependencies": { "typescript": "^5.0.0"