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
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"] }
|
||||
Loading…
Add table
Add a link
Reference in a new issue