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) <noreply@anthropic.com>
This commit is contained in:
parent
42f0aec52a
commit
db47037bdc
8 changed files with 124 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@ bootstrap/state/
|
||||||
# os
|
# os
|
||||||
.DS_Store
|
.DS_Store
|
||||||
provision/state/
|
provision/state/
|
||||||
|
offsite-backup/state/
|
||||||
|
offsite-backup/Pulumi.prod.yaml
|
||||||
|
provision/Pulumi.foundation-test.yaml
|
||||||
|
|
|
||||||
16
bun.lock
16
bun.lock
|
|
@ -22,6 +22,18 @@
|
||||||
"typescript": "^5.0.0",
|
"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": {
|
"packages/pulumi-docker": {
|
||||||
"name": "@olsitec/pulumi-docker",
|
"name": "@olsitec/pulumi-docker",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|
@ -160,6 +172,8 @@
|
||||||
|
|
||||||
"@olsitec/foundation-bootstrap": ["@olsitec/foundation-bootstrap@workspace:bootstrap"],
|
"@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/foundation-provision": ["@olsitec/foundation-provision@workspace:provision"],
|
||||||
|
|
||||||
"@olsitec/pulumi-docker": ["@olsitec/pulumi-docker@workspace:packages/pulumi-docker"],
|
"@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/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/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=="],
|
"@pulumi/random": ["@pulumi/random@4.21.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-k4JHqQFOKXWkbSLZtX37xBlRsurfgZbLd1Zib0cLSDP4Dt3uTllR3CBWfMLSnXJGAoTr9MlwyHCer/5tmSaF0Q=="],
|
||||||
|
|
|
||||||
|
|
@ -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).
|
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: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).
|
||||||
|
|
|
||||||
6
offsite-backup/Pulumi.yaml
Normal file
6
offsite-backup/Pulumi.yaml
Normal file
|
|
@ -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
|
||||||
86
offsite-backup/index.ts
Normal file
86
offsite-backup/index.ts
Normal file
|
|
@ -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);
|
||||||
6
offsite-backup/package.json
Normal file
6
offsite-backup/package.json
Normal file
|
|
@ -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" }
|
||||||
|
}
|
||||||
2
offsite-backup/tsconfig.json
Normal file
2
offsite-backup/tsconfig.json
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{ "compilerOptions": { "strict": true, "outDir": "bin", "target": "es2020", "module": "commonjs",
|
||||||
|
"moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true }, "files": ["index.ts"] }
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"bootstrap",
|
"bootstrap",
|
||||||
"provision"
|
"provision",
|
||||||
|
"offsite-backup"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue