Note
This is pre-release software. Use at your own risk.
A web application for managing SSH authorized_keys files across multiple hosts — React frontend, Python/FastAPI backend.
- Python 3.12+ with uv
- Node.js 24+ with npm
- just —
brew install just(or any installer from https://github.com/casey/just#installation) - Docker (optional, for production deployment)
just dev # starts backend + frontend in dev mode- Frontend: http://localhost:5173
- Backend API: http://localhost:8000/api/v2
Or individually:
just backend-install # uv sync
just backend-run # uvicorn with --reload
just frontend-install # npm install
just frontend-dev # vite dev serverRun just to see all available commands.
| Layer | Stack |
|---|---|
| Frontend | React 19 + TypeScript + Tailwind CSS + Vite |
| Backend | Python 3.12 + FastAPI + SQLAlchemy (async) |
| Database | SQLite (default) — swappable via DATABASE_URL |
| Migrations | Alembic |
| Auth | htpasswd credential store + JWT (HS256) |
| SSH | asyncssh |
The production Docker image is a single all-in-one container: nginx serves the built React SPA on port 80 and reverse-proxies /api/* to uvicorn on 127.0.0.1:8000. Migrations run automatically on startup.
just backend-test # pytest
just backend-test-cov # pytest with coverage report
just backend-lint # ruff check
just backend-fmt # ruff format
just backend-typecheck # mypy
just backend-security # banditjust frontend-lint # eslint
just frontend-typecheck # tsc --noEmit
just frontend-build # production buildjust migrate # alembic upgrade head
just migrate-new <name> # create autogenerated revision
just migrate-down # downgrade one step
just migrate-history # show migration historyEvery setting is read from environment variables. A backend/.env file is loaded automatically on startup via python-dotenv — copy backend/.env.example and edit. Shell variables always win over .env values. Set DOTENV=/path/to/file.env to load a different file.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:///ssm.db |
SQLAlchemy database URL |
JWT_SECRET |
— | Required in production (32+ chars) |
SSH_KEY |
keys/id_ssm |
Path to SSH private key |
SSH_KEY_PASSPHRASE |
— | Passphrase if the SSH key is encrypted |
SSH_TIMEOUT |
120 |
SSH connection timeout in seconds |
HTPASSWD |
.htpasswd |
Path to htpasswd credential file |
LOGLEVEL |
info |
Logging level |
PORT |
8000 |
Listen port |
LISTEN |
:: |
Listen address |
DOTENV |
./.env |
Path to the .env file to load |
SESSION_KEY is accepted as an alias for JWT_SECRET.
Generate a key for the server to use when connecting to managed hosts:
ssh-keygen -t ed25519 -f backend/keys/id_ssm -C 'ssm-server' -N ''The server auto-creates an .htpasswd file with a random admin password on first start (printed to console). To set credentials manually:
htpasswd -cB backend/.htpasswd admin # create file
htpasswd -B backend/.htpasswd user2 # add userThe combined image (ghcr.io/styliteag/ssm/ssm:latest) ships nginx + the SPA + the FastAPI backend in one container, listening on port 80.
just up # docker compose up -d --build
just down # docker compose down
just logs # tail logsThe compose file (docker/compose.prod.yml) expects persistent volumes at docker/data/:
docker/data/
├── config/.env # main config (optional, env vars)
├── config/.htpasswd # credentials
├── keys/id_ssm # SSH private key
├── db/ # SQLite database
└── logs/ # nginx + app logs
JWT_SECRET must be set — uncomment the line in compose.prod.yml and supply a 32+ character value.
To exercise the production image locally on http://localhost:8080 without affecting prod data:
just docker-build # build docker/app/Dockerfile (frontend + backend + nginx)
just docker-run # run it on :8080 with a scratch data dir under /tmp
just docker-stop # tear it downjust release # patch bump
just release minor # minor bump
just release major # major bumpBumps VERSION, commits, tags, and pushes. The tag push triggers the CI build.
- Disabled hosts: mark a host as disabled in the UI to skip all SSH operations (maintenance, decommission)
.ssh/system_readonlyon a remote host: prevents SSM from modifying any keyfile.ssh/user_readonlyon a remote host: prevents modifications for a specific user
Both files accept an optional reason string that is displayed in the UI.
- All
/api/v2endpoints require a valid JWT Bearer token (except/api/v2/auth/login) - Passwords stored as bcrypt hashes in htpasswd format
- JWT tokens signed with HS256; access and refresh tokens are distinct types (not interchangeable)
- SSH connections use key-based authentication only
GPL-3.0 — see LICENSE.txt.