Pinbox24 S3/Wasabi Configuration — bms-1 (w3 and w4)

Applies to: bms-1 (94.23.26.113) · Pinbox24 production (w3 = v3.2, w4 = v4.2) Last updated: 2026-06-29 Incident ref: 2026-06-29 — w3 upload broken (DO Spaces hardcoded endpoint)


1. Overview

Pinbox24 uses a single Wasabi bucket (pinbox24, region eu-central-1) for all file storage. 159,000+ objects going back to 2021 — treat as production-critical data.

DigitalOcean Spaces was legacy config only — files never actually went there. DO Spaces credentials (CY5VG3XCNNB4N3BZSLOE prefix) appeared in hardcoded configs but are invalid; investigate whether that key is still active (see Open Issues below).


2. Wasabi Bucket

ParameterValue
Bucketpinbox24
Regioneu-central-1
Endpoints3.eu-central-1.wasabisys.com
IAM userp24-access
Key limit2 active keys max (Wasabi IAM constraint)

3. Architecture

3.1 w3 (v3.2)

ComponentDetail
Backend containerv32-prod
Backend image563740926945.dkr.ecr.eu-central-1.amazonaws.com/v32-prod
Working dir/home/gitlab-runner/builds/eZQeLfuJe/0/pinbox24/p24-v-3.2/
Backend env filebackend-environment.env
S3 microservice containers3-v32-prod
S3 microservice imageECR (same account)
S3 microservice env files3-environment.env

The S3 microservice (s3-v32-prod) is a node-config application. It reads /app/config/default.js (hardcoded — see Warning below) and /app/config/local.js (override via env vars). Without local.js, all uploads go to the hardcoded DigitalOcean Spaces endpoint — which is wrong.

WARNING — local.js is NOT persistent. It is written directly into the running container via docker cp. It is lost on container recreation. A volume mount or image rebuild is required to make it permanent (see Open Issues).

3.2 w4 (v4.2)

ComponentDetail
Backend containerv42-prod
Backend image563740926945.dkr.ecr.eu-central-1.amazonaws.com/v42-prod
Working dir/root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/
Backend env filebackend-environment.env
S3 microservice (shared)s3-v32-prod (same container as w3)
S3-v2 microservices3-v2-v42-prod (TypeScript, v4-specific features)
S3-v2 working dir/root/builds/JsHnVmmJ/0/pinbox24/pinbox24-ms-s3-v2/
S3-v2 env files3-v2-environment.env

WARNING — v42-prod has HARDCODED Wasabi credentials in compiled TypeScript: /app/dist/globalHelpers/uploadAwsS3.helper.js

This file also contains an AWS key (AKIAYGQMT4PQ3ZLOWHRR prefix) — purpose unknown, needs investigation. The hardcoded Wasabi section causes getSignedUrl to return 403 Forbidden after any key rotation until manually patched. This patch is also NOT persistent across container recreation (see Open Issues).


4. Environment Variable Key Names

Env fileAccess key var nameSecret key var name
backend-environment.env (v32 and v42)s3Bucket_api_accessKeyIds3Bucket_api_secretAccessKey
s3-environment.env (v32 and v42)s3Bucket_api_accessKeyIds3Bucket_api_secretAccessKey
s3-v2-environment.envpinbox24PublicAccessKeyIdpinbox24PublicSecretAccessKey
uploadAwsS3.helper.js (hardcoded in v42)hardcoded string (patch required)hardcoded string (patch required)

Additional vars present in env files: s3Bucket_endpoint, s3Bucket_BUCKET_NAME.


5. Rotation Procedure

When the Wasabi key for p24-access needs to be rotated:

Step 1 — Rotate in Wasabi IAM

Wasabi console → IAM → Users → p24-access → Security Credentials
Delete old key → Create new key → Save (access key ID prefix + secret)

Wasabi enforces a 2-key maximum. You must delete the old key before creating a new one (or you already have 2 keys and must delete the inactive one).

Step 2 — Update env files on bms-1

SSH to root@94.23.26.113. Update key names s3Bucket_api_accessKeyId / s3Bucket_api_secretAccessKey in all five files:

# w3 backend
vi /home/gitlab-runner/builds/eZQeLfuJe/0/pinbox24/p24-v-3.2/backend-environment.env
 
# w3 S3 microservice
vi /home/gitlab-runner/builds/eZQeLfuJe/0/pinbox24/p24-v-3.2/s3-environment.env
 
# w4 backend
vi /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/backend-environment.env
 
# w4 S3 microservice (shared with w3, but separate env file for w4 working dir)
vi /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/s3-environment.env
 
# w4 S3-v2 (uses pinbox24Public* key names)
vi /root/builds/JsHnVmmJ/0/pinbox24/pinbox24-ms-s3-v2/s3-v2-environment.env

Step 3 — Recreate containers

Use the IMAGE_NAME/CONTAINER_NAME pattern — env vars must be set before docker-compose:

# w3 backend
cd /home/gitlab-runner/builds/eZQeLfuJe/0/pinbox24/p24-v-3.2
export CONTAINER_NAME=v32-prod
export IMAGE_NAME=$(docker inspect v32-prod --format "{{.Config.Image}}")
docker-compose up -d --force-recreate backend
 
# w3 S3 microservice
export CONTAINER_NAME=s3-v32-prod
export IMAGE_NAME=$(docker inspect s3-v32-prod --format "{{.Config.Image}}")
docker-compose up -d --force-recreate s3
 
# w4 backend
cd /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts
export CONTAINER_NAME=v42-prod
export IMAGE_NAME=$(docker inspect v42-prod --format "{{.Config.Image}}")
docker-compose up -d --force-recreate backend
 
# w4 S3-v2
cd /root/builds/JsHnVmmJ/0/pinbox24/pinbox24-ms-s3-v2
export CONTAINER_NAME=s3-v2-v42-prod
export IMAGE_NAME=$(docker inspect s3-v2-v42-prod --format "{{.Config.Image}}")
docker-compose up -d --force-recreate s3-v2

Step 4 — Restore local.js in s3-v32-prod (ALWAYS required after recreate)

The container loses /app/config/local.js on recreation. Write it back:

# Write local.js to a temp file on bms-1 (use actual values from env file)
cat > /tmp/local.js << 'EOF'
module.exports = {
  s3Bucket: {
    endpoint: process.env.s3Bucket_endpoint,
    accessKeyId: process.env.s3Bucket_api_accessKeyId,
    secretAccessKey: process.env.s3Bucket_api_secretAccessKey,
    bucket: process.env.s3Bucket_BUCKET_NAME,
  }
};
EOF
 
docker cp /tmp/local.js s3-v32-prod:/app/config/local.js
docker exec s3-v32-prod pm2 reload all
rm /tmp/local.js

Verify the process restarted:

docker exec s3-v32-prod pm2 list

Step 5 — Patch uploadAwsS3.helper.js in v42-prod (ALWAYS required after recreate)

This file has hardcoded Wasabi credentials. After recreating v42-prod, patch it:

# Copy out the file
docker cp v42-prod:/app/dist/globalHelpers/uploadAwsS3.helper.js /tmp/uploadAwsS3.helper.js
 
# Edit: replace the hardcoded accessKeyId and secretAccessKey strings with new values
# Key names: look for the old access key ID prefix and replace both strings
vi /tmp/uploadAwsS3.helper.js
 
# Copy back and reload
docker cp /tmp/uploadAwsS3.helper.js v42-prod:/app/dist/globalHelpers/uploadAwsS3.helper.js
docker exec v42-prod pm2 reload all
rm /tmp/uploadAwsS3.helper.js

Step 6 — Verify

# w3 upload test — from inside v32-prod or via API
# Expected response: {"success": true, "location": "https://pinbox24.s3.eu-central-1.wasabisys.com/..."}
curl -s -X POST https://<w3-domain>/api/files/<officeId>/upload -F file=@/tmp/test.txt
 
# w4 signed URL test
# Expected response: {"signedUrl": "https://..."}
curl -s https://<w4-domain>/api/offices/files/<fileId>/getSignedUrl

6. Diagnosis Procedure

Symptom: w3 uploads return no response / timeout

  1. Check s3-v32-prod logs: docker logs s3-v32-prod --tail 50
  2. Check if /app/config/local.js exists: docker exec s3-v32-prod cat /app/config/local.js
  3. If missing or showing DO Spaces endpoint → run Step 4 above (restore local.js)
  4. Confirm endpoint in use: docker exec s3-v32-prod env | grep s3Bucket_endpoint Expected: s3.eu-central-1.wasabisys.com

Symptom: w4 getSignedUrl returns 403 Forbidden

  1. Check v42-prod logs: docker logs v42-prod --tail 50
  2. The cause is almost certainly the hardcoded credentials in uploadAwsS3.helper.js
  3. Run Step 5 above to patch the file
  4. Verify the access key ID prefix in the patched file matches the current active Wasabi key

Symptom: s3-v2-v42-prod fails (w4 TypeScript S3 service)

  1. Check logs: docker logs s3-v2-v42-prod --tail 50
  2. Check env: docker exec s3-v2-v42-prod env | grep -i pinbox24
  3. Verify pinbox24PublicBucketName — currently set to test-replicated-to-us-bucket which may be incorrect (see Open Issues)

7. node-config Architecture (s3-v32-prod)

The S3 microservice uses the npm config package. Resolution order (later overrides earlier):

  1. /app/config/default.js — hardcoded, contains ams3.digitaloceanspaces.com (WRONG — legacy)
  2. /app/config/local.js — override written by ops, reads from process.env.s3Bucket_*

The service does NOT use custom-environment-variables.json, so raw env vars are ignored without local.js. This is the root cause of the 2026-06-29 incident.

Long-term fix required: rebuild the image with local.js baked in, or mount it via Docker volume.


8. Open Issues

#IssueSeverityStatus
1local.js in s3-v32-prod is not persistent — lost on container recreateHIGHFIXED 2026-06-29 — volume mount via persistent-patches/local.js in compose + containers recreated
2uploadAwsS3.helper.js patch in v42-prod is not persistentHIGHFIXED 2026-06-29 — volume mount via persistent-patches/uploadAwsS3.helper.js in docker-compose.yml + containers recreated
3s3-v2-v42-prod uses bucket test-replicated-to-us-bucket — is this intentional?MEDIUMopen — tracked in #2028; bucket confirmed on Wasabi eu-central-1
4AWS key AKIAYGQMT4PQ3ZLOWHRR found in uploadAwsS3.helper.js awsConf — purpose unknownMEDIUMopen — dead code; #2047 (human: AWS IAM console revoke)
5DO Spaces key prefix CY5VG3XCNNB4N3BZSLOE appeared in hardcoded config — verify if still activeLOWopen — #2046 (human: DO console revoke)
6s3-v32-prod logs the full MongoDB URI (with password) on startup — re-exposes the DB credential in PM2 logs on every restart, defeating rotationHIGH (P1)open — #2397 · fix procedure: pinbox24-s3-mongo-uri-log-redaction.md (persistent persistent-patches/ volume-mount, same pattern as 2). Root SSH to bms-1 required — human-action.
7W3 uploads broken after DB_URI migrated to rs0 — Mongoose 4.x (old-s3 image) uses OP_QUERY-wrapped inserts, incompatible with MongoDB 7.0.25HIGHFIXED 2026-07-01persistent-patches/controller.js bypasses MongoDB save: returns 200 + Wasabi file data on Mongoose error; filesUpload.js mirror insert writes metadata to w3_db on rs0 via mongojs. Volume mount added to docker-compose.yml s3 service.

9. Credential Incident Log

DateIncidentStatus
2026-06-29Old Wasabi key (VCUC7X6A1G... prefix) appeared in chat output during docker inspect and cat of uploadAwsS3.helper.jsKey was already deleted before exposure; no action required
2026-06-29pinbox24PublicSecretAccessKey value appeared in chat from docker inspect s3-v2-v42-prodSame old/deleted key; no action required

Rule: if a secret value appears in chat output, stop immediately, reference key NAME only, and follow docs/playbooks/static-api-key-incident-rotation.md.


  • docs/playbooks/static-api-key-incident-rotation.md — credential rotation procedure
  • docs/playbooks/sops-windows-crlf.md — safe Windows SOPS writes
  • secrets/bms-servers.env.sops — BMS root passwords (not Wasabi keys)
  • secrets/monitoring.env.sops — contains WASABI_ADMIN_ACCESS_KEY for IAM self-management

Wasabi IAM is managed via boto3 against iam.wasabisys.com using the admin key in secrets/administration.env.sops (key name: WASABI_ADMIN_ACCESS_KEY + WASABI_ADMIN_SECRET_KEY).


Audit Log — Log to infra_operations

After completing the key rotation (Step 6 verification passed), log it to the infra_operations audit table.

Python (Linux server — bms-4, vps-i1, or similar):

import sys
sys.path.insert(0, '/opt/p24-infra')
from scripts.lib.log_op import log_op
 
log_op(
    actor="claude",  # "radieu" for manual human ops, "claude" for agent
    op_type="config_change",
    resource="pinbox24-s3-wasabi",
    result="success",  # "success" | "failed" | "skipped"
    detail="Wasabi p24-access key rotated — env files updated on bms-1, containers recreated, local.js and uploadAwsS3.helper.js restored",
    env="bms-1",
    gh_issue=2730,  # replace with current working issue number if one exists
)

PowerShell (Windows dev machine):

$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'config_change', 'pinbox24-s3-wasabi', 'success', 'Wasabi p24-access key rotated — env files updated on bms-1, containers recreated', 'bms-1')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''

11. Post-Mortem: Inline Preview 403 (2026-07-03/04)

Root Causes

  1. RC-1: IAM s3v2-prod-bms1 brak dostepu do 3 bucketow plikow — policy pokrywala tylko stary bucket pinbox24; s3-v2 uzywa 3 bucketow (60% plikow niedostepnych).
  2. RC-2: Hardcoded usuniety klucz Wasabi w uploadAwsS3.helper.js — ccessKeyId: “VCUC7X6A1GINU54MDWON” (usuniety) + hardcoded endpoint us-east nie obslugiwany.
  3. RC-3: IAM pinbox24-bms1-s3 brak dostepu do 3 bucketow — getSignedUrl path tez bez uprawnien.
  4. RC-4: Wasabi EU IAM oddzielny od US IAM — zmiany przez iam.wasabisys.com nie propaguja do eu-central-1; wymagany iam.eu-central-1.wasabisys.com.

Fixes Applied (2026-07-04)

FixWynik
Policy s3v2-prod-bms1 — wszystkie 3 bucketyOK
Policy pinbox24-bms1-s3 — US + EU IAM endpointOK
Patch uploadAwsS3.helper.js — env vars + _bucketEndpointMapOK
Persistent patch — volume mount w docker-compose.ymlOK
docker-deploy-prod.sh — stare/usuniete klucze zastapioneOK

Patch Survival Matrix

ZdarzeniePrzezywa?
PM2 reload/restartTAK (plik na dysku)
docker restart v42-prodTAK (volume mount)
docker-compose up -dTAK (zmodyfikowany docker-compose.yml)
docker-deploy-prod.sh runTAK (skrypt naprawiony)
Swiezy git clone nadpisujacy build dirRYZYKO — git-deploy-v42-prod to tylko Discord webhook, nie klonuje autonomicznie

Wasabi EU IAM Discovery (CRITICAL)

Wasabi ma oddzielne IAM subsystemy:

  • US (us-east-1, us-east-2): iam.wasabisys.com
  • EU (eu-central-1): iam.eu-central-1.wasabisys.com

Policy update przez US endpoint zwraca HTTP 200 ale NIE propaguje do EU bucketow.

Persistent Patch Location

Host: /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/persistent-patches/uploadAwsS3.helper.js Mount: /app/dist/globalHelpers/uploadAwsS3.helper.js:ro w kontenerze v42-prod


12. Multi-Bucket Architecture (s3-v2-v42-prod)

s3-v2 uploaduje kazdy plik do 3 bucketow rownolegle (promiseAny — odpowiedz po pierwszym sukcesie; kazdy udany upload aktualizuje MongoDB storageInfo niezaleznie).

Buckety

BucketRegionDatacenter
test-replicated-to-us-bucketeu-central-1Frankfurt, DE
p24-was-us-east-1us-east-1Ashburn, VA
test-us-bucket-for-replication-testingus-east-2Columbus, OH

Nazwy z prefixem test-* sa mylace — to produkcyjne buckety od ok. 2022.

Dystrybucja plikow w4 (stan 2026-07-04)

MetrykaWartosc
Lacznie unikalnych plikow214,727
Pelna redundancja (3 kopie)127,374 (59%)
2 kopie41,623 (19%)
1 kopia (zagrozenie)45,730 (21%)
Job naprawczyGH issue #2709

IAM Users (po naprawie 2026-07-04)

IAM UserUzywaPolicy
s3v2-prod-bms1s3-v2-v42-prod (getBase64File)s3v2-prod-bucket-access — 3 buckety plikow
pinbox24-bms1-s3v42-prod (getSignedUrl)pinbox24-bucket-access — pinbox24 + 3 buckety plikow