Another S3 Manager
Lightweight, self-hosted web UI for managing files across multiple S3-compatible storage providers.
Works with AWS S3, MinIO, Cloudflare R2, Wasabi, and any S3-compatible API.
Quick Start
docker run -d -p 8080:8080 \
-e JWT_SECRET_KEY=$(openssl rand -base64 32) \
-v s3m-data:/app/data \
ghcr.io/kruchenburger/another-s3-manager:latest
Open http://localhost:8080 and log in with admin / change_me_pls. Persistent state
(SQLite DB, config.json, etc.) lives in the s3m-data named Docker volume — works out
of the box on Linux/macOS/Windows because Docker initializes named volumes with the
image's user ownership (the container runs as non-root app / uid 1001).
If you prefer a host-bind mount (-v $(pwd)/data:/app/data), on Linux you must
mkdir -p data && sudo chown 1001:1001 data first or the non-root user can't write.
For a docker compose setup see docker-compose.example.yml.
Features
- Multi-provider — connect AWS accounts, MinIO, R2, Wasabi in one UI
- Multi-account — switch between roles (default credentials, named profiles, assume role, direct keys)
- Fast file browser — table + grid views with image thumbnails; virtualized lists stay smooth in 10k+ object folders
- Upload & download — single files, multiple files, or entire folders; drag-and-drop supported
- Bulk operations — multi-select with bulk delete and bulk link copying
- File preview — images, video, PDF and text inline (text extensions are admin-configurable)
- Share links — presigned URLs with a configurable lifetime, per-link override up to 7 days
- Server-side search — prefix search inside folders too large to load client-side
- User management — per-role and per-bucket access, configurable password policy, forced first-login password change
- MCP server — AI agents (Claude Desktop, Cursor) browse the same storage with the same permissions
- Observability — Prometheus
/metricswith a ready-made Grafana dashboard, optional structured JSON logs (see docs/observability.md) - Security — JWT in an httpOnly cookie, bcrypt passwords, CSRF protection, per-username ban after failed logins
- Dark / light theme
Configuration
Configure roles through the web UI (Admin console → Roles; global settings live under
Admin console → Settings) or by editing config.json:
{
"roles": [
{
"name": "AWS Production",
"type": "assume_role",
"role_arn": "arn:aws:iam::123456789012:role/S3Access"
},
{
"name": "Dev Account",
"type": "credentials",
"access_key_id": "AKIA...",
"secret_access_key": "..."
},
{
"name": "Local MinIO",
"type": "credentials",
"access_key_id": "minioadmin",
"secret_access_key": "minioadmin",
"endpoint_url": "http://minio:9000",
"path_style": true
}
]
}
Role types: default, profile, assume_role, credentials. Any role can include endpoint_url, use_ssl, verify_ssl, path_style for S3-compatible services.
Upload limits
The maximum upload size is the admin-editable max_file_size config field
(Settings → General), falling back to the MAX_FILE_SIZE env var (default
100 MB). Uploads are guarded before the request body is read:
| Status | Meaning |
|---|---|
401 |
No or invalid session — the body is never received |
411 |
Missing Content-Length (chunked uploads are not accepted) |
413 |
Declared or actual size exceeds max_file_size |
Uploads stream to S3 via managed multipart, so files larger than 5 GB are
supported up to max_file_size (S3's per-object maximum is 5 TB). The MCP
upload_file tool uses a separate base64 path and keeps a 5 GB ceiling.
Environment Variables
| Variable | Description | Default |
|---|---|---|
JWT_SECRET_KEY |
Required. Secret for JWT tokens | — |
ADMIN_PASSWORD |
Admin password — seeds the admin on first boot and re-applies on change, until the password is set from the UI or CLI (see Authentication) | change_me_pls |
ADMIN_PASSWORD_FORCE |
One-shot override: 1/true/yes makes the next start apply ADMIN_PASSWORD even over a UI- or CLI-set password. Remove it afterwards |
unset |
PORT |
Server port | 8080 |
AWS_REGION |
Default AWS region | from env |
DATA_DIR |
Directory for SQLite DB and runtime data | /app/data |
MAX_FILE_SIZE |
Max upload size in bytes | 104857600 (100 MB) |
DISABLE_DELETION |
Disable delete operations | false |
COOKIE_SECURE |
Auth cookie Secure flag — set to false for local HTTP, true for HTTPS prod |
true |
JWT_ACCESS_TOKEN_EXPIRE_MINUTES |
Session lifetime in minutes | 180 |
PRESIGNED_URL_DEFAULT_TTL |
Default share-link lifetime in seconds | 3600 |
PRESIGNED_URL_MAX_TTL |
Max share-link lifetime in seconds (7-day SigV4 ceiling) | 604800 |
ENABLE_LAZY_LOADING |
Lazy loading for large file lists | true |
LOG_FORMAT |
Log output: text or json (for log aggregators) |
text |
METRICS_PASSWORD |
Basic-auth password for /metrics (endpoint is open if unset) |
— |
Authentication
Login issues an httpOnly + Secure + SameSite=Strict cookie carrying a JWT.
The cookie is not accessible to JavaScript (XSS-safe). CSRF protection
via X-CSRF-Token header on mutating requests; the CSRF token comes from
/api/me after login.
For local HTTP development, set COOKIE_SECURE=false or the browser will
silently drop the cookie (Secure flag requires HTTPS).
Admin password
The built-in admin user is created on first startup with the password from ADMIN_PASSWORD
(default change_me_pls — change it).
- Change
ADMIN_PASSWORDand restart and the new value is applied. Rotation works. - Change the password in the web UI (or with the reset CLI below) and the database wins from
then on:
ADMIN_PASSWORDwill not silently revert a password you chose yourself. To hand management back to the environment, setADMIN_PASSWORD_FORCE=1alongsideADMIN_PASSWORDfor one restart, then remove it — while it is set, the environment overrides the UI on every boot.
The app tells you which of these is in effect at startup, so if ADMIN_PASSWORD isn't doing what
you expect, the logs say why and what to do about it.
Lost the admin password?
Reset it from inside the container:
# Interactive — hidden prompt, asked twice (keeps it out of your shell history):
docker compose exec app python -m another_s3_manager.reset_admin_password
# Non-interactive (scripts / CI) — note the -T and --yes:
docker compose exec -T app python -m another_s3_manager.reset_admin_password 'NewPassword1' --yes
It enforces the same password policy as the UI and recreates the admin user if it was deleted.
Brute-force defense
Failed logins for non-admin accounts are tracked per username: 3 failed
attempts within 1 hour → account is banned for 1 hour. Admins are exempt
(the admin username is predictable, and a drive-by attacker could otherwise
lock the only admin out of the system).
There is no application-level IP rate limit — for production exposure, put the app behind a reverse proxy that handles rate-limiting and (optionally) SSO. See Production deployment below.
Production deployment
This app is an admin tool, not a public SaaS. The recommended deployment is behind an authenticated reverse proxy:
- Cloudflare Tunnel + Access — zero-trust SSO (Google, GitHub, email magic
link) gates
/loginbefore the request reaches the app. The login form is invisible to unauthenticated visitors. Free tier covers small teams. - nginx / Traefik / Caddy with basic-auth or OIDC — same idea, self-hosted.
- Cloudflare WAF rule — if you cannot put SSO in front, add a WAF rule
rate-limiting
POST /api/loginat the edge (e.g. 10/minute per IP).
For self-hosting on a single machine without external traffic, bind to
127.0.0.1 and use SSH tunnels or a local-only reverse proxy.
Storage
User accounts, ban records, and authentication state live in a SQLite database
(<DATA_DIR>/another_s3_manager.db). On first startup, if legacy
users.json or bans.json files are present, they are auto-imported
into the database and renamed to *.migrated.bak (kept as backup).
Configuration (config.json) remains a file — it's admin-edited
infrequently and benefits from human readability.
DB schema is managed via Alembic. Migrations run at startup
(alembic upgrade head).
Upgrading from v0.1.x? The upgrade is seamless (same volume, same env), with one caveat: the container now runs as a non-root user, so a data volume created by v0.1.x needs a one-time ownership fix. See the upgrade note in CHANGELOG.md.
Docker Compose
Two compose files ship with the repo:
docker-compose.yml— local dev. Builds the image from source, bind-mounts./datafor persistence.docker-compose.example.yml— self-host template. Pulls a published image from GHCR. Copy this onto a server, setJWT_SECRET_KEY, run.
For per-developer overrides (mounting ~/.aws for SSO profiles, custom volumes, etc.)
copy docker-compose.override.example.yml to
docker-compose.override.yml (gitignored, auto-loaded by compose).
Kubernetes
The container is Kubernetes-ready: read-only config.json mount via ConfigMap is supported,
SQLite DB lives under DATA_DIR (mount a PVC there), and secrets like JWT_SECRET_KEY come
from env. Helm charts for kruchenburger services live in a separate repo —
github.com/kruchenburger/helm (in progress).
Development
uv sync --all-extras # install dependencies (including dev: ruff, pytest, moto)
uv run pytest --cov # run tests
uv run ruff check . # lint
uv run ruff format . # format
Local server (no Docker):
JWT_SECRET_KEY=dev-secret uv run python -m another_s3_manager.main
# Persists SQLite + config.json under ./data (per .env.example)
Accessibility: see docs/accessibility.md for the
WCAG 2.1 AA baseline (axe-core via Playwright) and how to run the a11y spec
locally.
See docs/testing-backends.md for the MinIO vs ministack (AWS-native assume_role / credentials) test backends.
IAM Policy
Minimum permissions for full functionality:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::YOUR-BUCKET"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::YOUR-BUCKET/*"
}
]
}
What each action powers:
s3:ListBucket(on the bucket ARN) — browsing files.s3:GetObject— download / preview, object metadata, presigned share links, and the source of a copy/move.s3:PutObject— upload, and the destination of a copy/move.s3:DeleteObject— delete, and the source-removal half of a move/rename.
For a read-only deployment (or read-only MCP tokens), s3:ListBucket + s3:GetObject alone are enough.
Add s3:ListAllMyBuckets on * if you don't want to manually specify allowed buckets per role.
MCP server (for AI agents)
another-s3-manager exposes an MCP (Model Context Protocol) server at /mcp,
allowing AI agents (Claude Desktop, Cursor, Codex) to interact with your S3
storage using the same roles and permissions as the web UI.
Quick start: log in → User menu → MCP tokens → create. Copy the plaintext token once, paste into your AI agent's MCP config.
See docs/mcp-setup.md for client-specific configurations and security best practices.
No comments yet
Be the first to share your take.