Skip to content
michealzsPublic

About

SSH access provisioning: a Django REST API plus an idempotent CLI that writes ssh config, known_hosts, and authorized_keys with an audit trail

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ssh-provision-cli

A small provisioning system for client-side SSH access. It has two parts:

  • provision_api/ — a Django + Django REST Framework service that stores hosts, users, SSH public keys, host fingerprints, access grants, and an audit trail in SQLite, exposing a JSON API.
  • ssh_cli.py — a command-line tool that reads host/key data, records grants and audit entries through the API, and writes the local SSH files (config, known_hosts, authorized_keys) with the correct permissions.

The CLI is idempotent (re-running provision never duplicates entries) and revoke removes only the targeted host entries.

Installation

uv venv --python 3.12 .venv
uv pip install --python .venv -e .          # or: uv pip install -r requirements.txt

Dependencies: Django 5.0.x, djangorestframework 3.15.x, requests 2.32.x.

Configuration

Nothing is hard-coded; everything has a sensible default and can be overridden by a CLI flag or an environment variable.

Setting CLI flag Environment variable Default
API base URL --api-base PROVISION_API_BASE http://localhost:8000/api/v1
Database path --db-path PROVISION_DB_PATH ./db.sqlite3 (next to manage.py)
SSH target dir --target-dir PROVISION_TARGET_DIR the current user's home directory
SSH User --login-user — $USER

The Django service reads PROVISION_DB_PATH (and optionally PROVISION_SECRET_KEY / PROVISION_DEBUG) so the API and the CLI can share the same database.

Quick start

# 1. Prepare and seed the database
python manage.py migrate
python seed_data.py

# 2. Run the API
python manage.py runserver 0.0.0.0:8000

# 3. Provision access (writes files under <target-dir>/.ssh)
python ssh_cli.py --action provision --user alice \
    --hosts web-server-01,db-server-01 --target-dir /tmp/alice-home

# 4. Inspect status, then revoke one host
python ssh_cli.py --action status --user alice --target-dir /tmp/alice-home
python ssh_cli.py --action revoke --user alice --hosts web-server-01 \
    --target-dir /tmp/alice-home

After installation the CLI is also available as the ssh-provision console script.

API

Method & path Purpose Success status
POST /api/v1/grants Create grant(s) for a user/hosts 201
GET /api/v1/grants/<user> List a user's active grants 200
DELETE /api/v1/grants/<id> Revoke a grant 204
POST /api/v1/audit Record an audit entry 201

Endpoints accept both trailing-slash and non-trailing-slash forms.

Generated SSH files

  • config — one Host block per host (HostName, Port, User); mode 600.
  • known_hosts — host fingerprint lines; mode 644.
  • authorized_keys — the user's active public keys; mode 600.

Tests

.venv/bin/python -m pytest

The suite starts a live Django server against a temporary, seeded database, drives the CLI for provision/revoke/status, and asserts file contents, permissions, idempotency, revoke behaviour, and audit logging. Unit tests cover the file-handling helpers in isolation.

Seed data

seed_data.py loads four hosts (web-server-01, db-server-01, bastion-01, cache-server-01), three users (alice, bob, carol) with SSH keys, and the matching host fingerprints.

Running in production

The service is built to run as a container under gunicorn, with the operational concerns a real deployment needs.

Operational features

Concern How
Auth X-API-Key header, validated (constant-time) against PROVISION_API_KEYS. Unset ⇒ open (dev). The CLI sends PROVISION_API_KEY automatically.
Rate limiting DRF throttle, opt-in via PROVISION_RATE_LIMIT (e.g. 120/min). Counter is per-process unless PROVISION_CACHE_URL points at Redis.
Health probes GET /api/v1/health (liveness), GET /api/v1/readiness (DB check). Never gated by auth/throttle.
Metrics Prometheus at GET /metrics (django-prometheus).
Logging JSON lines to stdout (PROVISION_LOG_LEVEL).
Database SQLite by default; set PROVISION_DATABASE_URL for Postgres.
Static files Served by WhiteNoise (collected at image build).
Env var Default Meaning
PROVISION_DEBUG 1 0 for production
PROVISION_ALLOWED_HOSTS localhost,127.0.0.1,0.0.0.0 comma-separated
PROVISION_SECRET_KEY dev key set in production
PROVISION_API_KEYS — comma-separated keys; unset ⇒ auth off
PROVISION_RATE_LIMIT — e.g. 120/min; unset ⇒ no throttle
PROVISION_DATABASE_URL — Postgres URL; unset ⇒ SQLite
PROVISION_CACHE_URL — Redis URL for a global throttle
PROVISION_DB_PATH db.sqlite3 SQLite path
WEB_CONCURRENCY 2 gunicorn workers

Docker

docker build -t ssh-provision .
docker run -p 8000:8000 -e PROVISION_SEED=1 -v ssh_data:/data ssh-provision

The entrypoint runs migrations (and seeds when PROVISION_SEED=1), then serves with gunicorn. SQLite is persisted on the /data volume; a healthcheck hits /api/v1/health.

Compose, app only:

docker compose up -d --build

With a Postgres + Redis overlay (shared DB and global throttle):

docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d --build

Monitoring

A Prometheus + Grafana stack ships under a Compose profile:

docker compose --profile monitoring up -d --build

CI/CD

  • CI (.github/workflows/ci.yml) — Django check + ruff + pytest on Python 3.11/3.12, then a Docker build with a /health smoke test.
  • CD (.github/workflows/cd.yml) — builds and pushes an image to GHCR on v* tags / releases.

A Makefile wraps the common tasks (make to list them).

About

SSH access provisioning: a Django REST API plus an idempotent CLI that writes ssh config, known_hosts, and authorized_keys with an audit trail

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages