diff --git a/README.md b/README.md index 3634a86..64b66f0 100644 --- a/README.md +++ b/README.md @@ -306,21 +306,100 @@ bin/dev ## Production Deployment -### Docker +### Docker Compose (Recommended) + +Create a `docker-compose.yml` file: + +```yaml +services: + clinch: + image: ghcr.io/dkam/clinch:latest + ports: + - "127.0.0.1:3000:3000" # Bind to localhost only (reverse proxy on same host) + # Use "3000:3000" if reverse proxy is in Docker network or different host + environment: + # Rails Configuration + RAILS_ENV: production + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + + # Application Configuration + CLINCH_HOST: ${CLINCH_HOST} + CLINCH_FROM_EMAIL: ${CLINCH_FROM_EMAIL:-noreply@example.com} + + # SMTP Configuration + SMTP_ADDRESS: ${SMTP_ADDRESS} + SMTP_PORT: ${SMTP_PORT} + SMTP_DOMAIN: ${SMTP_DOMAIN} + SMTP_USERNAME: ${SMTP_USERNAME} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_AUTHENTICATION: ${SMTP_AUTHENTICATION:-plain} + SMTP_ENABLE_STARTTLS: ${SMTP_ENABLE_STARTTLS:-true} + + # OIDC Configuration (optional - generates temporary key if not provided) + OIDC_PRIVATE_KEY: ${OIDC_PRIVATE_KEY} + + # Optional Configuration + FORCE_SSL: ${FORCE_SSL:-false} + volumes: + - ./storage:/rails/storage + restart: unless-stopped +``` + +Create a `.env` file in the same directory: ```bash -# Build image -docker build -t clinch . +# Generate with: openssl rand -hex 64 +SECRET_KEY_BASE=your-secret-key-here -# Run container -docker run -p 3000:3000 \ - -v clinch-storage:/rails/storage \ - -e SECRET_KEY_BASE=your-secret-key \ - -e SMTP_ADDRESS=smtp.example.com \ - -e SMTP_PORT=587 \ - -e SMTP_USERNAME=your-username \ - -e SMTP_PASSWORD=your-password \ - clinch +# Application URLs +CLINCH_HOST=https://auth.yourdomain.com +CLINCH_FROM_EMAIL=noreply@yourdomain.com + +# SMTP Settings +SMTP_ADDRESS=smtp.example.com +SMTP_PORT=587 +SMTP_DOMAIN=yourdomain.com +SMTP_USERNAME=your-smtp-username +SMTP_PASSWORD=your-smtp-password + +# OIDC (optional - generates temporary key if not set) +# Generate with: openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 +# Then: OIDC_PRIVATE_KEY=$(cat private_key.pem) +OIDC_PRIVATE_KEY= + +# Optional: Force SSL redirects (if not behind a reverse proxy handling SSL) +FORCE_SSL=false +``` + +Start Clinch: + +```bash +docker compose up -d +``` + +**First Run:** +1. Visit `http://localhost:3000` (or your configured domain) +2. Complete the first-run wizard to create your admin account +3. Configure applications and invite users + +**Upgrading:** + +```bash +# Pull latest image +docker compose pull + +# Restart with new image (migrations run automatically) +docker compose up -d +``` + +**Logs:** + +```bash +# View logs +docker compose logs -f clinch + +# View last 100 lines +docker compose logs --tail=100 clinch ``` ### Backup & Restore @@ -351,9 +430,9 @@ sqlite3 storage/production.sqlite3 "VACUUM INTO 'backup-$(date +%Y%m%d).sqlite3' # 2. Backup uploaded files (ActiveStorage files are immutable) tar -czf uploads-backup-$(date +%Y%m%d).tar.gz storage/uploads/ -# Docker equivalent -docker exec clinch sqlite3 /rails/storage/production.sqlite3 "VACUUM INTO '/rails/storage/backup-$(date +%Y%m%d).sqlite3';" -docker exec clinch tar -czf /rails/storage/uploads-backup-$(date +%Y%m%d).tar.gz /rails/storage/uploads/ +# Docker Compose equivalent +docker compose exec clinch sqlite3 /rails/storage/production.sqlite3 "VACUUM INTO '/rails/storage/backup-$(date +%Y%m%d).sqlite3';" +docker compose exec clinch tar -czf /rails/storage/uploads-backup-$(date +%Y%m%d).tar.gz /rails/storage/uploads/ ``` **Restore:** @@ -380,13 +459,13 @@ sqlite3 /host/path/production.sqlite3 "VACUUM INTO '/host/path/backup-$(date +%Y rsync -av /host/path/backup-*.sqlite3 /host/path/uploads/ remote:/backups/clinch/ ``` -b) **Docker volumes** (e.g., `-v clinch_storage:/rails/storage`): +b) **Docker volumes** (e.g., using named volumes in compose): ```bash # Database backup (safe while running) -docker exec clinch sqlite3 /rails/storage/production.sqlite3 "VACUUM INTO '/rails/storage/backup.sqlite3';" +docker compose exec clinch sqlite3 /rails/storage/production.sqlite3 "VACUUM INTO '/rails/storage/backup.sqlite3';" # Copy out of container -docker cp clinch:/rails/storage/backup.sqlite3 ./backup-$(date +%Y%m%d).sqlite3 +docker compose cp clinch:/rails/storage/backup.sqlite3 ./backup-$(date +%Y%m%d).sqlite3 ``` **Option 2: While Stopped (Offline Backup)** @@ -411,35 +490,7 @@ docker compose up -d ## Configuration -### Environment Variables - -Create a `.env` file (see `.env.example`): - -```bash -# Rails -SECRET_KEY_BASE=generate-with-bin-rails-secret -RAILS_ENV=production - -# Database -# SQLite database stored in storage/ directory (Docker volume mount point) - -# SMTP (for sending emails) -SMTP_ADDRESS=smtp.example.com -SMTP_PORT=587 -SMTP_DOMAIN=example.com -SMTP_USERNAME=your-username -SMTP_PASSWORD=your-password -SMTP_AUTHENTICATION=plain -SMTP_ENABLE_STARTTLS=true - -# Application -CLINCH_HOST=https://auth.example.com -CLINCH_FROM_EMAIL=noreply@example.com - -# OIDC (optional - generates temporary key in development) -# Generate with: openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 -OIDC_PRIVATE_KEY= -``` +All configuration is handled via environment variables (see the `.env` file in the Docker Compose section above). ### First Run 1. Visit Clinch at `http://localhost:3000` (or your configured domain) diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 395b954..033512b 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -3,6 +3,7 @@ class InvitationsController < ApplicationController allow_unauthenticated_access before_action :set_user_by_invitation_token, only: %i[show update] + rate_limit to: 10, within: 10.minutes, only: :update, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." } def show # Show the password setup form diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index d253208..6b8bfad 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -2,6 +2,7 @@ class PasswordsController < ApplicationController allow_unauthenticated_access before_action :set_user_by_token, only: %i[edit update] rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." } + rate_limit to: 10, within: 10.minutes, only: :update, with: -> { redirect_to new_password_path, alert: "Too many attempts. Try again later." } def new end diff --git a/docs/beta-checklist.md b/docs/beta-checklist.md index 3f946a2..398262a 100644 --- a/docs/beta-checklist.md +++ b/docs/beta-checklist.md @@ -153,7 +153,7 @@ This checklist ensures Clinch meets security, quality, and documentation standar ### Deployment - [x] Docker support - [x] Docker Compose example -- [ ] Production deployment guide +- [x] Production deployment guide (Docker Compose with .env configuration, upgrading, logs) - [x] Backup and restore documentation ## Security Hardening @@ -165,10 +165,13 @@ This checklist ensures Clinch meets security, quality, and documentation standar - [x] Referrer-Policy (strict-origin-when-cross-origin in production config) ### Rate Limiting -- [ ] Login attempt rate limiting -- [ ] API endpoint rate limiting -- [ ] Token endpoint rate limiting -- [ ] Password reset rate limiting +- [x] Login attempt rate limiting (20/3min on sessions#create) +- [x] TOTP verification rate limiting (10/3min on sessions#verify_totp) +- [x] WebAuthn rate limiting (10/1min on webauthn endpoints, 10/3min on session endpoints) +- [x] Password reset rate limiting (10/3min on request, 10/10min on completion) +- [x] Invitation acceptance rate limiting (10/10min) +- [x] OAuth token endpoint rate limiting (60/1min on token, 30/1min on authorize) +- [x] Backup code rate limiting (5 failed attempts per hour, model-level) ### Secrets Management - [x] No secrets in code @@ -222,15 +225,15 @@ To move from "experimental" to "Beta", the following must be completed: - [x] All tests passing - [x] Core features implemented and tested - [x] Basic documentation complete +- [x] Backup/restore documentation +- [x] Production deployment guide - [ ] At least one external security review or penetration test -- [ ] Production deployment guide -- [ ] Backup/restore documentation **Important (Should have for Beta):** -- [ ] Rate limiting on auth endpoints -- [ ] Security headers configuration documented +- [x] Rate limiting on auth endpoints +- [x] Security headers configuration documented (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) +- [x] Known limitations documented (ForwardAuth same-domain requirement in README) - [ ] Admin audit logging -- [ ] Known limitations documented **Nice to have (Can defer to post-Beta):** - [ ] Bug bounty program @@ -250,16 +253,12 @@ To move from "experimental" to "Beta", the following must be completed: **Before Beta Release:** - 🔶 External security review recommended -- 🔶 Rate limiting implementation needed -- 🔶 Production deployment documentation -- 🔶 Security hardening checklist completion +- 🔶 Admin audit logging (optional) **Recommendation:** Consider Beta status after: 1. External security review or penetration testing -2. Rate limiting implementation -3. Production hardening documentation -4. 1-2 months of real-world testing +2. Real-world testing period --- -Last updated: 2026-01-01 +Last updated: 2026-01-02