From d90a06437e6295e04f2a9b164627f27d60cb107c Mon Sep 17 00:00:00 2001 From: Cory Hawkvelt Date: Sat, 4 Apr 2026 16:51:19 +1030 Subject: [PATCH] feat(docker): add Docker deployment configuration Add production-ready Docker setup with multi-stage Dockerfile, docker-compose orchestration for API, PostgreSQL, Redis, and Nginx services. Includes health checks, non-root user execution, and proper networking. - Add multi-stage Dockerfile with gunicorn/gevent workers - Add docker-compose.yml with api, db, redis, nginx services - Add nginx reverse proxy configuration with security headers - Update .env.example with Docker and production variables - Add email provider configuration (Mailgun, SendGrid) - Add requests dependency for HTTP client support - Update documentation with Docker deployment guide - Rebrand project name from Gatehouse to Secuird --- .env.example | 37 ++++++++- .gitignore | 1 + Dockerfile | 68 ++++++++++++++++ README.md | 116 ++++++++++++++++++++++++---- config/base.py | 14 +++- docker-compose.yml | 93 ++++++++++++++++++++++ docker/nginx.conf | 97 +++++++++++++++++++++++ requirements/base.txt | 3 + scripts/README.md | 4 +- scripts/configure_oauth_provider.py | 4 +- 10 files changed, 414 insertions(+), 23 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docker/nginx.conf diff --git a/.env.example b/.env.example index 537face..4fd332d 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,24 @@ FLASK_APP=manage.py FLASK_ENV=development FLASK_DEBUG=1 -# Database -DATABASE_URL=postgresql://user:password@localhost:5432/gatehouse_dev +# ═════════════════════════════════════════════════════════════════════════════ +# Docker / Production +# ═════════════════════════════════════════════════════════════════════════════ +COMPOSE_PROJECT_NAME=authy2 +FLASK_ENV=production +POSTGRES_USER=authy2 +POSTGRES_PASSWORD=changeme-in-production +POSTGRES_DB=authy2 +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} +SQLALCHEMY_DATABASE_URI=${DATABASE_URL} +REDIS_URL=redis://redis:6379/0 +SESSION_REDIS_URL=redis://redis:6379/0 +RATELIMIT_STORAGE_URL=redis://redis:6379/1 +HTTP_PORT=80 +HTTPS_PORT=443 +API_PORT=5000 + +# Database (overridden by Docker values above) SQLALCHEMY_ECHO=False SQLALCHEMY_LOG_LEVEL=WARNING @@ -15,7 +31,7 @@ CA_ENCRYPTION_KEY=change-me-in-production BCRYPT_LOG_ROUNDS=12 # Session cookies -SESSION_COOKIE_SECURE=False +SESSION_COOKIE_SECURE=True SESSION_COOKIE_SAMESITE=Lax # Only needed when sharing cookies across subdomains (e.g. api.example.com + ui.example.com) # SESSION_COOKIE_DOMAIN=example.com @@ -61,7 +77,7 @@ OIDC_BASE_URL=http://localhost:5000 # WebAuthn # ───────────────────────────────────────────────────────────────────────────── WEBAUTHN_RP_ID=localhost -WEBAUTHN_RP_NAME=Gatehouse +WEBAUTHN_RP_NAME=Secuird WEBAUTHN_ORIGIN=http://localhost:8080 # ───────────────────────────────────────────────────────────────────────────── @@ -81,6 +97,19 @@ SMTP_USERNAME= SMTP_PASSWORD= FROM_ADDRESS=noreply@gatehouse.local +# Email Provider (smtp, mailgun, sendgrid) +# Note: SMTP is the default. Set to "mailgun" or "sendgrid" to use those providers +EMAIL_PROVIDER=smtp + +# Mailgun Configuration (used when EMAIL_PROVIDER=mailgun) +# MAILGUN_API_KEY=your-mailgun-api-key +# MAILGUN_DOMAIN=mg.yourdomain.com +# MAILGUN_API_URL=https://api.mailgun.net/v3 + +# SendGrid Configuration (used when EMAIL_PROVIDER=sendgrid) +# SENDGRID_API_KEY=SG.your-sendgrid-api-key +# SENDGRID_FROM_EMAIL=noreply@yourdomain.com + # ───────────────────────────────────────────────────────────────────────────── # Logging # ───────────────────────────────────────────────────────────────────────────── diff --git a/.gitignore b/.gitignore index 118e26e..6fcc667 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ Thumbs.db # Project specific *.db +flask_session/ # Opencode files and folders .opencode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e795865 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# Multi-stage build for Gatehouse Auth API +# Build stage +FROM python:3.11-slim as builder + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy requirements files +WORKDIR /app +COPY requirements/base.txt requirements/base.txt +COPY requirements/production.txt requirements/production.txt + +# Install dependencies +RUN pip install --no-cache-dir --upgrade pip wheel && \ + pip install --no-cache-dir -r requirements/production.txt + +# Production stage +FROM python:3.11-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN groupadd --gid 1000 appgroup && \ + useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser + +# Copy virtual environment from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy application code +WORKDIR /app +COPY --chown=appuser:appgroup . . + +# Create log and session directories +RUN mkdir -p /app/logs /app/flask_session && chown -R appuser:appgroup /app/logs /app/flask_session + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:5000/api/health || exit 1 + +# Run gunicorn with gevent workers +CMD ["gunicorn", "--bind", "0.0.0.0:5000", \ + "--workers", "4", \ + "--worker-class", "gevent", \ + "--worker-connections", "1000", \ + "--timeout", "120", \ + "--access-logfile", "-", \ + "--error-logfile", "-", \ + "--log-level", "info", \ + "wsgi:application"] diff --git a/README.md b/README.md index 7cef62d..604bfc3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ python scripts/init_db.py 6. **Seed sample data** (optional): ```bash -python scripts/seed_data.py +python -m scripts.seed_data ``` 7. **Run the application**: @@ -77,6 +77,71 @@ python wsgi.py The API will be available at `http://localhost:5000` +## Docker Deployment + +### Prerequisites +- Docker 20.10+ +- Docker Compose 2.0+ + +### Quick Start + +1. **Start all services**: +```bash +docker-compose up -d +``` + +2. **Initialize the database** (run migrations): +```bash +docker-compose exec api python manage.py db upgrade +``` + +3. **Seed sample data** (optional): +```bash +docker-compose exec api python scripts/seed_data.py +``` + +4. **Verify health**: +```bash +curl http://localhost:5000/api/health +``` + +### Useful Commands + +```bash +# View logs +docker-compose logs -f api + +# Run migrations +docker-compose exec api python manage.py db upgrade + +# Open shell in container +docker-compose exec api /bin/bash + +# Rebuild after changes +docker-compose up -d --build + +# Stop all services +docker-compose down +``` + +### Environment Variables + +Copy `.env.example` to `.env` and configure: +- `POSTGRES_USER` / `POSTGRES_PASSWORD` - Database credentials +- `SECRET_KEY` - Flask secret key (required in production) +- `ENCRYPTION_KEY` - Data encryption key +- `CA_ENCRYPTION_KEY` - CA private key encryption +- `CORS_ORIGINS` - Allowed CORS origins (comma-separated) + +### Production Considerations + +- Use a strong `SECRET_KEY` (256-bit random) +- Enable HTTPS via nginx (configure SSL certificates) +- Set `BCRYPT_LOG_ROUNDS=13` for stronger password hashing +- Use Redis persistence (`--appendonly yes`) +- Configure log aggregation as needed + + ## API Endpoints ### Authentication @@ -197,22 +262,45 @@ python manage.py db upgrade -## running seed -python -m scripts.seed_data +## Development Commands -## Running flask in dev +### Run Flask in Development +```bash FLASK_ENV=development flask run --debug --port 8888 +``` + +### Seed Sample Data +```bash +python -m scripts.seed_data +# Or with Docker: +docker-compose exec api python scripts/seed_data.py +``` + +### Database Migration +```bash +# Apply migrations +flask db upgrade + +# With Docker: +docker-compose exec api python manage.py db upgrade +``` + +### SQLite Browser (Development) +```bash +sqlite_web instance/db_file.db --port 9999 --host 0.0.0.0 +``` -# Test creds -## OIDC Client -client_id: acme-portal-001 -client_secret: acme_secret_portal_2024 +## Test Credentials -## User -email: bob@acme-corp.com -password: UserPass123! +### OIDC Client +| Field | Value | +|-------|-------| +| client_id | `acme-portal-001` | +| client_secret | `acme_secret_portal_2024` | - -## Sqlite editor -sqlite_web instance/db_file.db --port 9999 --host 0.0.0.0 \ No newline at end of file +### Test User +| Field | Value | +|-------|-------| +| email | `bob@acme-corp.com` | +| password | `UserPass123!` | \ No newline at end of file diff --git a/config/base.py b/config/base.py index 2ac6adb..cf94022 100644 --- a/config/base.py +++ b/config/base.py @@ -123,7 +123,7 @@ class BaseConfig: # WebAuthn Configuration WEBAUTHN_RP_ID = os.getenv("WEBAUTHN_RP_ID", "localhost") - WEBAUTHN_RP_NAME = os.getenv("WEBAUTHN_RP_NAME", "Gatehouse") + WEBAUTHN_RP_NAME = os.getenv("WEBAUTHN_RP_NAME", "Secuird") WEBAUTHN_ORIGIN = os.getenv("WEBAUTHN_ORIGIN", "https://ui.webauthn.local") # Frontend URL (for OAuth callback redirects) @@ -140,3 +140,15 @@ class BaseConfig: SMTP_USERNAME = os.getenv("SMTP_USERNAME", "") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") FROM_ADDRESS = os.getenv("FROM_ADDRESS", "noreply@gatehouse.local") + + # Email Provider Configuration + EMAIL_PROVIDER = os.getenv("EMAIL_PROVIDER", "smtp").lower() # smtp, mailgun, sendgrid + + # Mailgun Configuration (used when EMAIL_PROVIDER=mailgun) + MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "") + MAILGUN_DOMAIN = os.getenv("MAILGUN_DOMAIN", "") + MAILGUN_API_URL = os.getenv("MAILGUN_API_URL", "https://api.mailgun.net/v3") + + # SendGrid Configuration (used when EMAIL_PROVIDER=sendgrid) + SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY", "") + SENDGRID_FROM_EMAIL = os.getenv("SENDGRID_FROM_EMAIL", "") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b2b9b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,93 @@ +version: '3.8' + +services: + api: + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + environment: + - FLASK_ENV=production + - CORS_ORIGINS=http://192.168.50.124:8080,http://localhost:8080,http://localhost:5173 + - DATABASE_URL=postgresql://${POSTGRES_USER:-gatehouse}:${POSTGRES_PASSWORD:-gatehouse}@db:5432/${POSTGRES_DB:-gatehouse} + - SQLALCHEMY_DATABASE_URI=postgresql://${POSTGRES_USER:-gatehouse}:${POSTGRES_PASSWORD:-gatehouse}@db:5432/${POSTGRES_DB:-gatehouse} + - REDIS_URL=redis://redis:6379/0 + - SESSION_REDIS_URL=redis://redis:6379/0 + - RATELIMIT_STORAGE_URL=redis://redis:6379/1 + ports: + - "${API_PORT:-5000}:5000" + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - authy2-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + db: + image: postgres:16-alpine + environment: + - POSTGRES_USER=${POSTGRES_USER:-gatehouse} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-gatehouse} + - POSTGRES_DB=${POSTGRES_DB:-gatehouse} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - authy2-network + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-gatehouse} -d ${POSTGRES_DB:-gatehouse}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + ports: + - "5432:5432" + + redis: + image: redis:7-alpine + command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru + volumes: + - redis_data:/data + networks: + - authy2-network + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + nginx: + image: nginx:1.27-alpine + volumes: + - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "${HTTP_PORT:-80}:80" + - "${HTTPS_PORT:-443}:443" + depends_on: + - api + networks: + - authy2-network + restart: unless-stopped + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + authy2-network: + driver: bridge + +volumes: + postgres_data: + redis_data: diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..ff9c194 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,97 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types text/plain text/css text/xml application/json application/javascript + application/xml application/xml+rss text/javascript application/x-javascript; + + upstream api { + server api:5000; + } + + server { + listen 80; + server_name localhost; + + # Health check endpoint + location /health { + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # API routes + location /api/ { + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + + # Increase buffer for larger responses + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 16k; + proxy_busy_buffers_size 24k; + } + + # Catch-all proxy (for any other routes) + location / { + proxy_pass http://api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + } +} diff --git a/requirements/base.txt b/requirements/base.txt index 1f6591d..a96d6c7 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -49,5 +49,8 @@ Flask-Limiter==3.5.0 python-json-logger==2.0.7 qrcode[pil] +# HTTP requests +requests>=2.31.0 + # SSH CA Certificate signing sshkey-tools==0.11.3 diff --git a/scripts/README.md b/scripts/README.md index 683c66f..b9d2a27 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,6 @@ -# Gatehouse Scripts +# Secuird Scripts -This directory contains utility scripts for managing and configuring Gatehouse. +This directory contains utility scripts for managing and configuring Secuird. ## OAuth Provider Configuration Script diff --git a/scripts/configure_oauth_provider.py b/scripts/configure_oauth_provider.py index 5372bd7..77d460d 100755 --- a/scripts/configure_oauth_provider.py +++ b/scripts/configure_oauth_provider.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -OAuth Provider Configuration Script for Gatehouse +OAuth Provider Configuration Script for Secuird This script allows administrators to configure OAuth providers at the application level using the new ApplicationProviderConfig architecture. @@ -457,7 +457,7 @@ def delete_provider(args): def main(): """Main entry point for the script.""" parser = argparse.ArgumentParser( - description="Configure OAuth providers for Gatehouse authentication", + description="Configure OAuth providers for Secuird authentication", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: