A self-hosted customer portal that lets MinIO IAM users securely browse their buckets, view storage usage, and manage their own service accounts (access keys) — without exposing the native MinIO console.
Built with SvelteKit 2 + Svelte 5, deployed as a standalone Node.js server via @sveltejs/adapter-node.
- Secure login — MinIO IAM credentials (access key + secret key) verified directly against the MinIO S3 API; nothing stored except an encrypted session token
- Dashboard — bucket cards showing used storage, quota bar, object count, last upload date, and tags
- Bucket explorer — breadcrumb navigation, object listing, one-click presigned downloads
- Service accounts — create and delete your own child access keys; secret shown once on creation, never again
- Portal admin — local admin account (no MinIO IAM required) with a full cluster management UI
- Multi-cluster — configure any number of MinIO clusters; users are automatically routed to the correct one after a user sync
- Dark / light theme — system preference detected, toggle in navbar
- Security hardened — AES-256-GCM credentials at rest, rate-limited login, CSP / security headers, HttpOnly SameSite cookies, per-cluster TLS control
- Node.js 20+
mc(MinIO Client) installed and onPATH(or setMC_PATH)- A MinIO cluster with at least one IAM user
git clone https://github.com/nimbustech-lab/s3-portal.git
cd s3-portal
npm installcp .env.example .envEdit .env — the minimum required fields:
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
SESSION_ENCRYPTION_KEY=<64 hex chars>
PORTAL_ADMIN_USER=admin
PORTAL_ADMIN_PASSWORD=<strong password>S3 cluster credentials can be added via the Admin UI after first login, or pre-seeded in .env:
CLUSTER_1_ID=silo
CLUSTER_1_LABEL=SILO Cluster
CLUSTER_1_ENDPOINT=s3.example.com:9000
CLUSTER_1_SECURE=true
CLUSTER_1_ADMIN_ACCESS=<admin access key>
CLUSTER_1_ADMIN_SECRET=<admin secret key>npm run build
node build/index.jsThe server listens on http://0.0.0.0:8900 by default (override with PORT and HOST env vars).
- Open the portal URL and log in with
PORTAL_ADMIN_USER/PORTAL_ADMIN_PASSWORD - Go to Admin → Add Cluster and enter your MinIO cluster details
- Click Sync Users to pull IAM accounts from MinIO into the portal DB
- MinIO users can now log in — the portal auto-routes them to the correct cluster
src/
├── lib/server/
│ ├── clusterStore.ts # cluster CRUD (DB + .env merged), user sync, login resolution
│ ├── config.ts # .env cluster parsing (CLUSTER_N_* variables)
│ ├── crypto.ts # AES-256-GCM encrypt/decrypt for credentials at rest
│ ├── db.ts # SQLite via better-sqlite3 (WAL mode)
│ ├── mc.ts # mc CLI wrapper — admin ops, user/SA listing
│ ├── portalAdmin.ts # portal admin local auth (separate from MinIO IAM)
│ ├── ratelimit.ts # in-memory IP rate limiter (10 req / 15 min)
│ ├── s3.ts # MinIO SDK — bucket list, object list, presigned URLs
│ └── session.ts # session create/get/delete with encrypted credential storage
└── routes/
├── login/ # S3 credential login + portal admin login
├── logout/ # cookie clear
├── (protected)/
│ ├── +layout.server.ts # session guard — redirects to /login if unauthenticated
│ ├── admin/ # cluster CRUD + user sync (portal admin only)
│ ├── dashboard/ # bucket overview cards
│ ├── explorer/[bucket]/ # object browser
│ └── service-accounts/ # access key management
└── api/download/[bucket]/ # presigned URL redirect
| Table | Purpose |
|---|---|
sessions |
Active sessions — encrypted access key + secret key |
db_clusters |
Clusters created via Admin UI — encrypted admin credentials |
cluster_users |
IAM users synced from each cluster |
service_accounts_sync |
Service account access keys per user (secret keys never stored) |
| Variable | Required | Default | Description |
|---|---|---|---|
SESSION_ENCRYPTION_KEY |
Yes | — | 64 hex chars (32 bytes) for AES-256-GCM |
PORTAL_ADMIN_USER |
Yes | — | Portal admin username |
PORTAL_ADMIN_PASSWORD |
Yes | — | Portal admin password |
CLUSTER_N_ID |
No | — | Cluster identifier (slug) |
CLUSTER_N_LABEL |
No | — | Display name |
CLUSTER_N_ENDPOINT |
No | — | host:port |
CLUSTER_N_SECURE |
No | true |
Use HTTPS |
CLUSTER_N_INSECURE |
No | false |
Allow self-signed TLS cert |
CLUSTER_N_ADMIN_ACCESS |
No | — | Admin access key |
CLUSTER_N_ADMIN_SECRET |
No | — | Admin secret key |
CLUSTER_MAX |
No | 10 |
Max number of CLUSTER_N_* entries to scan |
DB_PATH |
No | ./data/portal.db |
SQLite database path |
MC_PATH |
No | /usr/local/bin/mc |
Path to mc binary |
PORT |
No | 8900 |
HTTP listen port |
HOST |
No | 0.0.0.0 |
HTTP listen address |
# /etc/systemd/system/s3portal.service
[Unit]
Description=Nimbus S3 Customer Portal
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/s3portal
EnvironmentFile=/opt/s3portal/.env
ExecStart=/usr/bin/node build/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now s3portalExpose via nginx or HAProxy with SSL termination on port 443.
- Admin credentials live only in server-side env vars and encrypted DB; never sent to the browser
- Session data island contains only
{ id, username, cluster_id }— no access key or secret key - Service account secrets are returned once at creation and never stored in the portal
- Per-cluster TLS —
CLUSTER_N_INSECURE=trueapplies a customhttps.Agentonly to that cluster; the globalNODE_TLS_REJECT_UNAUTHORIZEDis never modified - Rate limiting — 10 failed login attempts per IP per 15-minute window
- CSP —
Content-Security-Policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-originapplied via SvelteKithandlehook
cp .env.example .env # fill in SESSION_ENCRYPTION_KEY and admin creds
npm run dev # starts on http://localhost:8900 with HMRMIT