Container-native SFTP gateway with OpenSSH, chroot isolation, and declarative user provisioning.
Project description
Container-native SFTP for teams that want a small, auditable OpenSSH runtime with declarative users, predictable Docker deployment, and a CLI that works the same locally and on remote hosts.
Key Features · Installation · Quick Start · Deployment · Providers · Docs · Contributing
SFTPWarden runs OpenSSH in a container and keeps users, host keys, data, and runtime
state outside the image. You manage environments with sftpwarden, and the runtime
keeps Linux users synchronized from YAML, CSV, SQLite, MySQL, MariaDB,
PostgreSQL, or MongoDB.
Table of Contents
- Key Features
- Installation
- Shell Autocomplete
- 5-Minute Quick Start
- Deployment Choices
- Project Files
- User Management
- Providers
- Operations
- Security
- Documentation
- Roadmap
- Contributing
Key Features
- Fast adoption for real SFTP needs: create a local or remote SFTP environment
with
sftpwarden init, add users, and deploy with Docker Compose without hand-writing OpenSSH container plumbing. - Declarative user sources: manage accounts from YAML, CSV, SQLite, MySQL, MariaDB, PostgreSQL, or MongoDB, so small teams can start with files and larger systems can use databases.
- Safe user isolation: every SFTP user is forced into OpenSSH
internal-sftpand isolated under/data/<username>with chroot-oriented defaults. - Docker-native operations: generated Compose files,
sftpwarden deploy,plan,refresh,watch,--dry-run, and--jsonmake it practical for local development, CI, and production runbooks. - Context-based workflow: use Docker-style active contexts for
dev,prod, remote local-sync, and remote-only deployments instead of repeating long flags on every command. - Remote deployment built in: deploy through SSH, rsync, and Docker Compose, with systemd or Docker watcher modes for syncing user-provider changes.
- Portable operations: copy users between providers, export/import user snapshots, create backups, restore safely, and run project/runtime healthchecks.
- Operationally conservative defaults: secrets are not baked into images, plaintext provider passwords are rejected, host keys and state are persisted, and user data is never deleted unless explicitly requested.
SFTPWarden is intentionally lightweight. It is not a full identity platform, a file sharing suite, or VM-grade isolation. It gives you a conservative OpenSSH-based SFTP runtime that is easy to understand, deploy, and operate.
Installation
Install the CLI:
pip install sftpwarden
sftpwarden --version
For local development:
git clone https://github.com/kithuto/sftpwarden.git
cd sftpwarden
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[mysql,postgres,mongodb]"
sftpwarden --version
Optional extras:
| Need | Install |
|---|---|
| SQLite provider | Included, no extra |
| MySQL provider | pip install "sftpwarden[mysql]" |
| MariaDB provider | pip install "sftpwarden[mariadb]" |
| PostgreSQL provider | pip install "sftpwarden[postgres]" |
| MongoDB provider | pip install "sftpwarden[mongodb]" |
| Documentation/development | pip install -e ".[dev,docs,mysql,postgres,mongodb]" |
mariadb is an alias of the MySQL extra. Installing either
sftpwarden[mysql] or sftpwarden[mariadb] enables both MySQL and MariaDB
providers because they share PyMySQL.
Build the runtime image locally:
docker build -t sftpwarden:local -f docker/runtime/Dockerfile .
Shell Autocomplete
SFTPWarden can install shell autocomplete through the Typer/Click helpers included in the CLI:
sftpwarden --install-completion
Open a new terminal, then use <TAB> to complete commands and options:
sftpwarden con<TAB>
sftpwarden user add --<TAB>
To inspect the generated completion script without installing it:
sftpwarden --show-completion
5-Minute Quick Start
Create a local SFTP project:
sftpwarden config default-provider yaml
mkdir -p ~/sftpwarden-dev
cd ~/sftpwarden-dev
sftpwarden init dev --yes
sftpwarden validate
sftpwarden deploy
Add a user:
sftpwarden user add alice \
--password "correct horse battery staple" \
--comment "Main upload account"
Connect with any SFTP client:
sftp -P 2222 alice@localhost
Preview and apply runtime changes:
sftpwarden plan
sftpwarden refresh
sftpwarden init makes the created context active, so day-to-day commands do not
need --context. This follows the same friendly idea people know from Docker:
work in a project directory, keep an active context, and pass an explicit context
only when you need to override it. To switch later, run sftpwarden context use dev.
Read or update project settings without opening YAML:
sftpwarden config project.name dev2
sftpwarden config server.port 2200
sftpwarden context root ~/sftpwarden-dev2 --yes
Deployment Choices
Pick the model that matches how your team works.
| Model | Best for | Source of truth | Watcher |
|---|---|---|---|
| Local | Development, demos, single-host testing | Local project folder | No |
| Remote local-sync | Production managed from a workstation or CI runner | Local project folder synced to remote host | Yes |
| Remote-only | Existing remote deployments managed in-place | Remote project folder | No |
Local:
mkdir -p ~/sftpwarden-dev
cd ~/sftpwarden-dev
sftpwarden init dev --yes
sftpwarden deploy
Remote local-sync:
mkdir -p ~/sftpwarden-prod
cd ~/sftpwarden-prod
sftpwarden init prod --remote deploy@sftp-prod.example.com:/opt/sftpwarden \
--critical
sftpwarden deploy --dry-run
sftpwarden deploy --yes
Remote-only:
sftpwarden init archive --remote deploy@sftp-archive.example.com:/opt/sftpwarden \
--remote-only \
--critical
sftpwarden refresh --dry-run
Use sftpwarden context add when a SFTPWarden project already exists and you only
want to register it on this machine:
sftpwarden context add prod deploy@sftp-prod.example.com:/opt/sftpwarden --critical
sftpwarden context use prod
Production-like names such as prod, production, prd, live, and main
require confirmation unless marked with --critical or accepted with --yes.
Project Files
sftpwarden init creates a small project directory:
sftpwarden.yaml
users.yaml # or users.csv / users.sqlite
docker-compose.yml
data/
state/
host_keys/
The container always listens on port 22 internally. Configure the host-facing
port with server.port in sftpwarden.yaml.
Runtime state is stored in /var/lib/sftpwarden/state.json inside the container
and should be backed by the state/ volume. Host keys are stored in host_keys/
to keep SSH fingerprints stable across restarts.
User Management
List and inspect users:
sftpwarden users
sftpwarden user show alice
Add users:
sftpwarden user add alice --password "correct horse battery staple"
sftpwarden user add bob --password-hash '$6$rounds=500000$...'
sftpwarden user add carol --public-key "ssh-ed25519 AAAA..."
Update users:
sftpwarden user update alice --upload-dir inbound
sftpwarden user update alice --uid 12001 --gid 12001
sftpwarden user update alice --comment "Finance inbox"
sftpwarden user update alice --disabled
Removing a user disables access but does not delete user data:
sftpwarden user remove alice --yes
To permanently remove the user's data directory too:
sftpwarden user remove alice --delete-files --yes
Updating only comment does not refresh the runtime because comments are metadata.
Providers
| Provider | Runtime reads | CLI mutations | Good fit |
|---|---|---|---|
| YAML | Yes | Yes | Quick start, GitOps-style small deployments |
| CSV | Yes | Yes | Spreadsheet-friendly user handoff |
| SQLite | Yes | Yes | Single-host/self-hosted deployments without an external database |
| MySQL | Yes | Yes | Existing application databases |
| MariaDB | Yes | Yes | MySQL-compatible MariaDB deployments |
| PostgreSQL | Yes | Yes | Existing platform or product databases |
| MongoDB | Yes | Yes | Existing document databases |
SQL providers read from sftp_users by default. The table should include:
username, public_keys, password_hash, uid, gid, upload_dir, comment, disabled
See examples/mysql/schema.sql, examples/mariadb/schema.sql, and
examples/postgres/schema.sql.
SQLite is built in:
sftpwarden init dev --provider sqlite --yes
SQLite is a good lightweight option for one host and one writer. Avoid it for NFS, high-concurrency, or multi-writer deployments.
During init, SFTPWarden checks whether external provider storage exists. For
MySQL, MariaDB, and PostgreSQL that means the configured SQL table. For MongoDB
that means the configured collection and username index. If storage is missing,
interactive init asks whether to create it or abort so you can create it manually:
sftpwarden init prod \
--provider postgresql \
--dsn 'postgresql://sftpwarden:change-me@db.example.com:5432/sftpwarden' \
--create-table
MariaDB uses the same compatible implementation as MySQL:
sftpwarden init prod \
--provider mariadb \
--dsn 'mariadb://sftpwarden:change-me@db.example.com:3306/sftpwarden' \
--create-table
MongoDB stores one document per user with _id = username:
sftpwarden init prod \
--provider mongodb \
--dsn 'mongodb://mongo.example.com:27017/sftpwarden' \
--collection sftp_users
--dsn uses the standard database URL/DSN convention:
postgresql://user:password@host:5432/database
mysql://user:password@host:3306/database
mariadb://user:password@host:3306/database
mongodb://user:password@host:27017/database
For real environments, prefer an environment variable so the secret is not typed directly in shell history or committed in project files:
export SFTPWARDEN_POSTGRES_DSN='postgresql://sftpwarden:change-me@db.example.com:5432/sftpwarden'
sftpwarden init prod \
--provider postgresql \
--dsn '${SFTPWARDEN_POSTGRES_DSN}' \
--create-table
If you run interactive init with MySQL, MariaDB, or PostgreSQL and omit
--dsn, SFTPWarden asks for host, port, database, username, and password, then
writes the equivalent DSN for you. For MongoDB, interactive init asks for the
MongoDB DSN directly.
Operations
Common operational commands:
sftpwarden doctor
sftpwarden validate --config sftpwarden.yaml
sftpwarden compose --write
sftpwarden deploy --dry-run
sftpwarden plan --json
sftpwarden refresh --dry-run
sftpwarden watcher status --json
sftpwarden health --json
sftpwarden backup --output sftpwarden-dev.tar.gz --yes
sftpwarden provider export --format json > users.json
watch and refresh are different on purpose:
sftpwarden watchsyncs YAML/CSV/SQLite user provider files for remotelocal-synccontexts.sftpwarden refreshtells a running runtime to reload users immediately.- Configuration and Docker Compose changes require
sftpwarden deploy. sftpwarden healthvalidates config, provider readability, Compose drift, and runtime health where available.sftpwarden backupstores config, provider snapshot, host keys, and runtime state. It excludesdata/unless--include-datais explicitly used.sftpwarden provider copymoves users between contexts/providers with explicit--mergeor--replacesemantics.
sftpwarden deploy checks for Docker Compose before starting local deployments.
For remote deployments, the remote host must have Docker Compose v2 available as
docker compose.
For production watcher installs, prefer systemd so SSH uses the host's normal
identity, agent, ~/.ssh/config, known hosts, bastions, and ProxyJump settings.
Docker watcher mode is stricter and requires explicit dedicated deployment keys.
Security
SFTPWarden follows conservative defaults:
- users and secrets are not baked into images;
- plaintext passwords are rejected in provider data;
sftpwarden user add --passwordstores only a system password hash;- SFTP users are forced into
internal-sftp; - root login, empty passwords, forwarding, tunneling, X11, and user environments are disabled;
- user data is not deleted automatically;
sftpwarden user remove --delete-filesis explicit and irreversible;.env,data/,state/,host_keys/, Git metadata, and Python caches are not watched or synced.
Key-only deployment:
auth:
allow_public_key: true
allow_password: false
recommended: public_key
Read the security guide before exposing a deployment to a public or customer-facing network.
Documentation
The README is the adoption path. Detailed guides live in:
The Sphinx documentation is built from docs/ and published to GitHub Pages.
Build it locally:
python -m pip install -e ".[docs]"
sphinx-build -b html docs docs/_build/html
Roadmap
v1.1 - Providers, Backup, Import/Export, and Health
Released in 1.1.0: SQLite, MariaDB, MongoDB, provider transfer,
backup/restore, and healthchecks.
v1.2 - Kubernetes
- Add Helm chart and Kubernetes manifests.
- Add ConfigMap/Secret/PVC examples.
- Add liveness/readiness probes.
Contributing
Contributions are welcome: bug reports, docs fixes, examples, tests, provider work, and operational feedback are all useful.
Contribution workflow:
- Fork the repository.
- Create your own branch from
dev. - Develop and validate your change in that branch.
- Open a Pull Request from your branch to
dev.
Normal contribution PRs should target dev, not main. The maintainer promotes
accepted changes from dev to main for production and release work.
Start here:
- CONTRIBUTING.md for the GitHub workflow.
- docs/contributing.md for install, development, testing, docs, and release checks.
- SECURITY.md for responsible vulnerability reporting.
- CODE_OF_CONDUCT.md for participation expectations.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sftpwarden-1.1.0.tar.gz.
File metadata
- Download URL: sftpwarden-1.1.0.tar.gz
- Upload date:
- Size: 623.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a216eb48b7f30c4fde780da38610876e96439fdf38429df572d9591a98fa5988
|
|
| MD5 |
76ee1abfcad20f210d02065bce28ce40
|
|
| BLAKE2b-256 |
1bd96ab408b1b5cfd4534ab9c46a6dab17c8eb23c1140f751678b011d47813d0
|
Provenance
The following attestation bundles were made for sftpwarden-1.1.0.tar.gz:
Publisher:
release.yml on kithuto/SFTPWarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sftpwarden-1.1.0.tar.gz -
Subject digest:
a216eb48b7f30c4fde780da38610876e96439fdf38429df572d9591a98fa5988 - Sigstore transparency entry: 1928243079
- Sigstore integration time:
-
Permalink:
kithuto/SFTPWarden@fcf22c4b19989da4feb9cab2eed4fd26e1ed4230 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/kithuto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fcf22c4b19989da4feb9cab2eed4fd26e1ed4230 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sftpwarden-1.1.0-py3-none-any.whl.
File metadata
- Download URL: sftpwarden-1.1.0-py3-none-any.whl
- Upload date:
- Size: 103.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf5ecd35fafdda2488fc40c3be91b8085a8eb095fbaeb1e2fa46de7d3233868c
|
|
| MD5 |
741cd44558ca372c64cf48bdb2a169e6
|
|
| BLAKE2b-256 |
169c3416e8319ce418b72ba4a55d03185d8ae9a1f5b427f27eaddf77751395bf
|
Provenance
The following attestation bundles were made for sftpwarden-1.1.0-py3-none-any.whl:
Publisher:
release.yml on kithuto/SFTPWarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sftpwarden-1.1.0-py3-none-any.whl -
Subject digest:
cf5ecd35fafdda2488fc40c3be91b8085a8eb095fbaeb1e2fa46de7d3233868c - Sigstore transparency entry: 1928243196
- Sigstore integration time:
-
Permalink:
kithuto/SFTPWarden@fcf22c4b19989da4feb9cab2eed4fd26e1ed4230 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/kithuto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fcf22c4b19989da4feb9cab2eed4fd26e1ed4230 -
Trigger Event:
push
-
Statement type: